/*
 * Copyright (C) 2010 Fores Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package jp.fores.midori.client;

import java.net.URL;

import jp.fores.midori.client.handler.RPCHandler;
import jp.fores.midori.client.serializer.RPCSerializer;
import jp.fores.midori.core.dto.RPCInputDto;
import jp.fores.midori.core.util.ClassUtil;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;


/**
 * リモートプロシージャーコールを行うクライアントクラス。<br>
 * (このクラスはスレッドセーフではありません。)<br>
 */
public class RPCClient {
	//==========================================================
	//定数

	/**
	 * ログ出力用
	 */
	private static final Log log = LogFactory.getLog(RPCClient.class);

	/**
	 * デフォルトのリクエストメソッド
	 */
	public static final String DEFAULT_REQUEST_METHOD = "POST";

	/**
	 * デフォルトのリクエスト送信時にgzip圧縮を行うかどうかのフラグ
	 */
	public static final boolean DEFAULT_REQUEST_GZIP = false;

	/**
	 * デフォルトのレスポンスでgzip圧縮を受け入れるかどうかのフラグ
	 */
	public static final boolean DEFAULT_RESPONSE_GZIP = false;

	/**
	 * デフォルトの接続タイムアウト時間(単位=ミリ秒)
	 */
	public static final int DEFAULT_CONNECT_TIMEOUT = 200;

	/**
	 * デフォルトの読み取りタイムアウト時間(単位=ミリ秒)
	 */
	public static final int DEFAULT_READ_TIMEOUT = 0;

	/**
	 * デフォルトのGAE用かどうかのフラグ
	 */
	public static final boolean DEFAULT_IS_GAE = false;


	//==========================================================
	//フィールド

	/**
	 * リモートプロシージャーコールの各種形式に応じたシリアライズ・デシリアライズ処理を行うクラス
	 */
	public final RPCSerializer serializer;

	/**
	 * リモートプロシージャーコールを行うクライアントの通信処理を行うハンドラ
	 */
	public final RPCHandler handler;

	/**
	 * リクエストメソッド
	 */
	public String requestMethod = DEFAULT_REQUEST_METHOD;

	/**
	 * リクエスト送信時にgzip圧縮を行うかどうかのフラグ
	 */
	public boolean requestGZip = DEFAULT_REQUEST_GZIP;

	/**
	 * GAE用かどうかのフラグ
	 */
	public boolean isGAE = DEFAULT_IS_GAE;

	/**
	 * レスポンスでgzip圧縮を受け入れるかどうかのフラグ
	 */
	public boolean responseGZip = DEFAULT_RESPONSE_GZIP;

	/**
	 * 接続タイムアウト時間(単位=ミリ秒)
	 */
	public int connectTimeout = DEFAULT_CONNECT_TIMEOUT;

	/**
	 * 読み取りタイムアウト時間(単位=ミリ秒)
	 */
	public int readTimeout = DEFAULT_READ_TIMEOUT;


	//==========================================================
	//メソッド

	//_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/
	//コンストラクタ

	/**
	 * コンストラクタです。<br>
	 *
	 * @param serializer リモートプロシージャーコールの各種形式に応じたシリアライズ・デシリアライズ処理を行うクラス
	 * @param handler リモートプロシージャーコールを行うクライアントの通信処理を行うハンドラ
	 * @throws IllegalArgumentException 引数にnullが指定された場合
	 */
	public RPCClient(RPCSerializer serializer, RPCHandler handler)
			throws IllegalArgumentException {
		//==========================================================
		//引数のチェック

		//リモートプロシージャーコールの各種形式に応じたシリアライズ・デシリアライズ処理を行うクラスがnullの場合
		if (serializer == null) {
			//不正な引数の場合の例外を投げる
			throw new IllegalArgumentException("リモートプロシージャーコールの各種形式に応じたシリアライズ・デシリアライズ処理を行うクラスにnullを指定することはできません。");
		}

		//リモートプロシージャーコールを行うクライアントの通信処理を行うハンドラがnullの場合
		if (handler == null) {
			//不正な引数の場合の例外を投げる
			throw new IllegalArgumentException("リモートプロシージャーコールを行うクライアントの通信処理を行うハンドラにnullを指定することはできません。");
		}


		//==========================================================
		//引数の値をフィールドに設定

		//リモートプロシージャーコールの各種形式に応じたシリアライズ・デシリアライズ処理を行うクラス
		this.serializer = serializer;

		//リモートプロシージャーコールを行うクライアントの通信処理を行うハンドラ
		this.handler = handler;
	}


	//_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/
	//通常のメソッド

	/**
	 * リモートプロシージャコールを行います。<br>
	 *
	 * @param url URL
	 * @param methodName メソッド名
	 * @param args メソッドに渡す引数(可変引数)
	 * @return リモートプロシージャコールの戻り値のオブジェクト(戻り値が存在しない場合はnull)
	 * @throws Exception 例外
	 */
	public Object remoteCall(String url, String methodName, Object... args)
			throws Exception {
		//メソッド名と引数から入力データ用のDTOをシリアライズしたバイト配列を作成して、
		//実際に処理を行うメソッドを呼び出す
		return remoteCall(url, createRPCInputDto(methodName, args));
	}

	/**
	 * 戻り値の型指定でリモートプロシージャコールを行います。<br>
	 *
	 * @param <T> 戻り値の型
	 * @param returnClass 戻り値の型のクラスオブジェクト
	 * @param url URL
	 * @param methodName メソッド名
	 * @param args メソッドに渡す引数(可変引数)
	 * @return リモートプロシージャコールの戻り値のオブジェクト(戻り値が存在しない場合はnull)
	 * @throws Exception 例外
	 */
	public <T> T remoteCall(Class<T> returnClass, String url,
			String methodName, Object... args) throws Exception {
		//メソッド名と引数から入力データ用のDTOをシリアライズしたバイト配列を作成して、
		//実際に処理を行うメソッドを呼び出す
		Object result = remoteCall(url, createRPCInputDto(methodName, args));

		//戻り値の型補正を行って返す
		return ClassUtil.adjustObjectType(result, returnClass);
	}

	/**
	 * Content-Encoding用のヘッダ名を返します。<br>
	 *
	 * @return Content-Encoding用のヘッダ名
	 */
	public String getContentEncodingHeaderName() {
		//GAE用の場合
		if (this.isGAE) {
			//GAE用の特殊な値を返す
			return "x-GAE-Content-Encoding";
		}
		//それ以外の場合
		else {
			//普通の値を返す
			return "Content-Encoding";
		}
	}

	/**
	 * Accept-Encoding用のヘッダ名を返します。<br>
	 *
	 * @return Accept-Encoding用のヘッダ名
	 */
	public String getAcceptEncodingHeaderName() {
		//GAE用の場合
		if (this.isGAE) {
			//GAE用の特殊な値を返す
			return "x-GAE-Accept-Encoding";
		}
		//それ以外の場合
		else {
			//普通の値を返す
			return "Accept-Encoding";
		}
	}


	//_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/
	//内部処理用

	/**
	 * メソッド名と引数からリモートプロシージャーコールの入力データを格納するためのDTOクラスのインスタンスを作成します。
	 *
	 * @param methodName メソッド名
	 * @param args メソッドに渡す引数(可変引数)
	 * @return リモートプロシージャーコールの入力データを格納するためのDTO
	 */
	protected RPCInputDto createRPCInputDto(String methodName, Object... args) {
		//デバッグログが有効な場合
		if (log.isDebugEnabled()) {
			//デバッグログ出力
			log.debug("サーバーの「" + methodName + "()」メソッドを呼び出すための入力データのDTOを作成します。");
		}

		//リモートプロシージャーコールの入力データを格納するためのDTOクラスのインスタンスを生成
		RPCInputDto inputDto = new RPCInputDto();

		//入力データ用のDTOに引数のメソッド名と引数の配列を設定
		inputDto.methodName = methodName;
		inputDto.args = args;

		//入力データ用のDTOに設定した引数の配列がnullの場合
		if (inputDto.args == null) {
			//入力データ用のDTOの引数の配列に空配列を設定し直す
			inputDto.args = new Object[0];
		}

		//作成した入力データ用のDTOを返す
		return inputDto;
	}

	/**
	 * 入力データ用のDTOを元にして、リモートプロシージャコールを行います。
	 *
	 * @param url URL
	 * @param inputDto 入力データ用のDTO
	 * @return リモートプロシージャコールの戻り値のオブジェクト(戻り値が存在しない場合はnull)
	 * @throws Exception 例外
	 */
	protected Object remoteCall(String url, RPCInputDto inputDto)
			throws Exception {
		//デバッグログが有効な場合
		if (log.isDebugEnabled()) {
			//デバッグログ出力
			log.debug("*** 「" + url + "」の" + "「" + inputDto.methodName
					+ "」メソッドに対するリモートプロシージャコールを行います ***");
			log.debug("requestMethod=" + this.requestMethod + ", requestGZip="
					+ this.requestGZip + ", responseGZip=" + this.responseGZip
					+ ", isGAE=" + this.isGAE + ", connectTimeout="
					+ this.connectTimeout + ", readTimeout=" + this.readTimeout);
		}

		//urlをURLクラスでラッピングし、ハンドラに処理を移譲する
		return this.handler.execute(this, new URL(url), inputDto);
	}
}
